package compiler

import (
	"kumachan/lang/source"
	"kumachan/lang/typsys"
	"kumachan/interpreter/program"
)


func assign(expected typsys.Type, expr *program.Expr, ctx *exprContext, s0 *typsys.InferringState) (*program.Expr, *typsys.InferringState, *source.Error) {
	var loc = expr.Info.Location
	var t = expr.Type.Type
	// direct
	if ok, s1 := typsys.Match(expected, t, s0); ok {
		return expr, s1, nil
	}
	// convert to interface
	if I, Id, Ia, ok := getInterface(expected, ctx); ok {
		var d, a, ok = getTypeDefArgs(t, ctx)
		if !(ok) { goto NG }
		if _, is_interface := d.Content.(typsys.Interface); is_interface {
			// interface -> interface
			var ok, s1 = typsys.MatchAll(Ia, a, s0)
			if !(ok) { goto NG }
			{ var It, ok = typsys.GetCertainOrInferred(expected, s1)
			if !(ok) { goto NG }
			if up, ok := getInterfacePath(d, Id, ctx, nil); ok {
				var converted = &program.Expr {
					Type:    It,
					Info:    expr.Info,
					Content: program.InterfaceTransformUpward {
						Arg:  expr,
						Path: up,
					},
				}
				return converted, s1, nil
			} else {
				goto NG
			} }
		} else {
			// concrete -> interface
			var table, ok = ctx.resolveDispatchTable(d, Id)
			if !(ok) {
				if isSamInterface(I, Id) {
					var field = I.FieldList[0]
					var field_t = inflateFieldType(field, Id, Ia)
					var ok, s1 = typsys.Match(field_t, t, s0)
					if !(ok) { goto NG }
					{ var It, ok = typsys.GetCertainOrInferred(expected, s1)
					if !(ok) { goto NG }
					var converted = &program.Expr {
						Type: It,
						Info: expr.Info,
						Content: program.InterfaceFromSingleMethod {
							MethodValue: expr,
						},
					}
					return converted, s1, nil }
				}
				goto NG
			}
			{ var ok, s1 = typsys.MatchAll(Ia, a, s0)
			if !(ok) { goto NG }
			{ var It, ok = typsys.GetCertainOrInferred(expected, s1)
			if !(ok) { goto NG }
			var converted = &program.Expr {
				Type:    It,
				Info:    expr.Info,
				Content: program.Interface {
					ConcreteValue: expr,
					DispatchTable: table,
				},
			}
			return converted, s1, nil } }
		}
	}
	// convert to union
	if U, Ud, Ua, ok := getUnion(expected, ctx); ok {
		var converted *program.Expr
		var s2 *typsys.InferringState
		var key string
		var found = false
		for i, field := range U.FieldList {
			var index = uint(i)
			var field_t = inflateFieldType(field, Ud, Ua)
			var ok, s1 = typsys.Match(field_t, t, s0)
			if ok {
				var certain, ok = typsys.GetCertainOrInferred(expected, s1)
				if ok {
					if found {
						var this_key = field.Name
						return nil, nil, source.MakeError(loc,
							E_AmbiguousAssignmentToUnion {
								Union: Ud.Ref.String(),
								Key1:  key,
								Key2:  this_key,
							})
					}
					found = true
					key = field.Name
					s2 = s1
					converted = &program.Expr {
						Type:    certain,
						Info:    expr.Info,
						Content: program.Union {
							Index: int(index),
							Value: expr,
						},
					}
				}
			}
		}
		if found {
			return converted, s2, nil
		} else {
			goto NG
		}
	}
	// convert to Int
	if program.T_Int_(expected) {
		if _, _, ok := getEnum(t, ctx); ok {
			var converted = &program.Expr {
				Type:    typsys.CertainType { Type: program.T_Int() },
				Info:    expr.Info,
				Content: program.EnumToInt { EnumValue: expr },
			}
			return converted, s0, nil
		}
	}
	NG:
	return nil, nil, source.MakeError(loc,
		E_NotAssignable {
			From: typsys.Describe(t),
			To:   typsys.DescribeWithInferringState(expected, s0),
		})
}

func getUnion(t typsys.Type, ctx *exprContext) (typsys.Union, *typsys.TypeDef, ([] typsys.Type), bool) {
	var def, args, ok = getTypeDefArgs(t, ctx)
	if !(ok) { goto NG }
	{ var union, ok = def.Content.(typsys.Union)
	if !(ok) { goto NG }
	return union, def, args, true }
	NG:
	return typsys.Union {}, nil, nil, false
}
func getEnum(t typsys.Type, ctx *exprContext) (typsys.Enum, *typsys.TypeDef, bool) {
	var def, _, ok = getTypeDefArgs(t, ctx)
	if !(ok) { goto NG }
	{ var enum, ok = def.Content.(typsys.Enum)
	if !(ok) { goto NG }
	return enum, def, true }
	NG:
	return typsys.Enum {}, nil, false
}
func getTypeDefArgs(t typsys.Type, ctx *exprContext) (*typsys.TypeDef, ([] typsys.Type), bool) {
	if ref_type, ok := t.(typsys.RefType); ok {
		var def = ctx.mustHaveType(ref_type.Def)
		var args = ref_type.Args
		return def, args, true
	} else {
		return nil, nil, false
	}
}
func getInterfacePath(root *typsys.TypeDef, descendant *typsys.TypeDef, ctx *exprContext, base ([] int)) ([] int, bool) {
	for i, child_ref := range root.Interfaces {
		var child = ctx.mustHaveType(child_ref)
		var current = append(base, i)
		if child == descendant {
			return current, true
		} else {
			var path, ok = getInterfacePath(child, descendant, ctx, current)
			if ok {
				return path, true
			}
		}
	}
	return nil, false
}


